翻译:云荒杯倾
Embind用于绑定C++函数和类到JavaScript,这样编译代码就能在js中以一种很自然的方式来使用。Embind也支持从C++调JavaScript的class。
Embind支持绑定大多数C++的结构,包括C++11和C++14中引入的。它只有一个明显的限制就是目前还不支持raw pointers with complicated lifetime semantics。
本文展示了如何使用EMSCRIPTEN_BINDINGS()块来创建函数、类、值类型、指针(包括原始和智能指针)、枚举和常量的绑定,以及如何为抽象类创建绑定,这些抽象类可以在JavaScript中被重写。它还简要介绍了如何管理传递给JavaScript的c++对象句柄的内存。
note:
Embind的灵感来自 Boost.Python,他们使用非常相似的方法定义绑定。
一个简单例子
下面的代码使用EMSCRIPTEN_BINDINGS()暴露了C++ lerp()函数给JavaScript。
// quick_example.cpp
#include <emscripten/bind.h>
using namespace emscripten;
float lerp(float a, float b, float t) {
return (1 - t) * a + t * b;
}
EMSCRIPTEN_BINDINGS(my_module) {
function("lerp", &lerp);
}
为了使用embind编译上例,请调用emcc的bing选项:
emcc --bind -o quick_example.js quick_example.cpp
生成的quick_example.js文件可以作为node模块加载,也可以使用<script>加载:
<!doctype html>
<html>
<script src="quick_example.js"></script>
<script>
console.log('lerp result: ' + Module.lerp(1, 2, 0.5));
</script>
</html>
当quick_example.js文件初始化加载后, EMSCRIPTEN_BINDINGS()中代码会运行。
所有通过Embind暴露的symblols都可以在Module对象获取。
类
暴露一个类给JavaScript需要比较复杂的绑定语句,比如:
class MyClass {
public:
MyClass(int x, std::string y)
: x(x)
, y(y)
{}
void incrementX() {
++x;
}
int getX() const { return x; }
void setX(int x_) { x = x_; }
static std::string getStringFromInstance(const MyClass& instance) {
return instance.y;
}
private:
int x;
std::string y;
};
// Binding code
EMSCRIPTEN_BINDINGS(my_class_example) {
class_<MyClass>("MyClass")
.constructor<int, std::string>()
.function("incrementX", &MyClass::incrementX)
.property("x", &MyClass::getX, &MyClass::setX)
.class_function("getStringFromInstance", &MyClass::getStringFromInstance)
;
}
绑定块在一个临时class_对象上定义了成员函数调用链(Boost.Python也是同样风格)。
note:
你应该只绑定那些你实际需要的项(将它作为一个规则或原则),因为每个绑定会增加代码大小。比如,内部方法和私有变量可以很少绑定。
在JavaScript中定义和使用MyClass实例的代码如下:
var instance = new Module.MyClass(10, "hello");
instance.incrementX();
instance.x; // 12
instance.x = 20; // 20
Module.MyClass.getStringFromInstance(instance); // "hello"
instance.delete();
内存管理
因为JavaScript,尤其是ECMA-262 Edition 5.1,不支持 finalizers or weak references with callbacks,因此Emscripten没有办法调用C++对象的析构函数。
警告:
JavaScript代码必须明确删除C++对象的句柄,否则Emscripten堆会无限增长。
var x = new Module.MyClass;
x.method();
x.delete();
var y = Module.myFunctionThatReturnsClassInstance();
y.method();
y.delete();
值类型
对基本类型进行手动内存管理是麻烦的,所以embind对值类型提供了支持。包括Value arrays和 value objects,分别对应js的array和object。
示例:
struct Point2f {
float x;
float y;
};
struct PersonRecord {
std::string name;
int age;
};
PersonRecord findPersonAtLocation(Point2f);
EMSCRIPTEN_BINDINGS(my_value_example) {
value_array<Point2f>("Point2f")
.element(&Point2f::x)
.element(&Point2f::y)
;
value_object<PersonRecord>("PersonRecord")
.field("name", &PersonRecord::name)
.field("age", &PersonRecord::age)
;
function("findPersonAtLocation", &findPersonAtLocation);
}
以下代码就不需要担心手动生命周期管理。
var person = Module.findPersonAtLocation([10.2, 156.5]);
console.log('Found someone! Their name is ' + person.name + ' and they are ' + person.age + ' years old');
高级类概念(todo)
重载函数
构造函数和函数可以根据参数数量重载,但embind不支持根据参数类型重载。当你指定一个重载,请使用select_overload()帮助函数选中合适的签名。
struct HasOverloadedMethods {
void foo();
void foo(int i);
void foo(float f) const;
};
EMSCRIPTEN_BINDING(overloads) {
class_<HasOverloadedMethods>("HasOverloadedMethods")
.function("foo", select_overload<void()>(&HasOverloadedMethods::foo))
.function("foo_int", select_overload<void(int)>(&HasOverloadedMethods::foo))
.function("foo_float", select_overload<void(float)const>(&HasOverloadedMethods::foo))
;
}
枚举
embind支持C++98枚举和C++11枚举类。
enum OldStyle {
OLD_STYLE_ONE,
OLD_STYLE_TWO
};
enum class NewStyle {
ONE,
TWO
};
EMSCRIPTEN_BINDINGS(my_enum_example) {
enum_<OldStyle>("OldStyle")
.value("ONE", OLD_STYLE_ONE)
.value("TWO", OLD_STYLE_TWO)
;
enum_<NewStyle>("NewStyle")
.value("ONE", NewStyle::ONE)
.value("TWO", NewStyle::TWO)
;
}
JavaScript调用方式如下:
Module.OldStyle.ONE;
Module.NewStyle.TWO;
常量
向JavaScript暴露一个常量:
EMSCRIPTEN_BINDINGS(my_constant_example) {
constant("SOME_CONSTANT", SOME_CONSTANT);
}
内存视图
在某些情况下,将原始二进制数据以一个类型化数组的形式直接暴露给JavaScript代码是有价值的。这对于直接从堆上上传大型WebGL纹理非常有用。
内存视图应该像指针一样对待;生命周期和有效性不由运行时管理的,如果底层对象被修改或重新分配,则很容易损坏数据。
#include <emscripten/bind.h>
#include <emscripten/val.h>
using namespace emscripten;
unsigned char *byteBuffer = /* ... */;
size_t bufferLength = /* ... */;
val getBytes() {
return val(typed_memory_view(bufferLength, byteBuffer));
}
EMSCRIPTEN_BINDINGS(memory_view_example) {
function("getBytes", &getBytes);
}
下面JavaScript代码接收类型数组视图
var myUint8Array = Module.getBytes()
var xhr = new XMLHttpRequest();
xhr.open('POST', /* ... */);
xhr.send(myUint8Array);
使用val将JavaScript翻译为C++
Embind提供了一个c++类,emscripten::val,您可以使用它将JavaScript代码转换为c++。使用val,可以在c++中调用JavaScript对象,读取和写入它们的属性,或者强制它们成为c++值,比如bool、int或std::string。
下面代码展示了你可以通过val在C++中调用JavaScript的 Web Audio API。
首先看一下js的代码,展示js怎么用这个API:
// Get web audio api context
var AudioContext = window.AudioContext || window.webkitAudioContext;
// Got an AudioContext: Create context and OscillatorNode
var context = new AudioContext();
var oscillator = context.createOscillator();
// Configuring oscillator: set OscillatorNode type and frequency
oscillator.type = 'triangle';
oscillator.frequency.value = 261.63; // value in hertz - middle C
// Playing
oscillator.connect(context.destination);
oscillator.start();
// All done!
然后使用val将代码翻译成c++,如下:
#include <emscripten/val.h>
#include <stdio.h>
#include <math.h>
using namespace emscripten;
int main() {
val AudioContext = val::global("AudioContext");
if (!AudioContext.as<bool>()) {
printf("No global AudioContext, trying webkitAudioContext\n");
AudioContext = val::global("webkitAudioContext");
}
printf("Got an AudioContext\n");
val context = AudioContext.new_();
val oscillator = context.call<val>("createOscillator");
printf("Configuring oscillator\n");
oscillator.set("type", val("triangle"));
oscillator["frequency"].set("value", val(261.63)); // Middle C
printf("Playing\n");
oscillator.call<void>("connect", context["destination"]);
oscillator.call<void>("start", 0);
printf("All done!\n");
}
首先使用global()取全局AudioContext对象(如果不存在就取webkitAudioContext对象),然后使用new_()创建实例,从实例我们可以创建oscillator,设置set()它的属性,然后播放。
内建类型转换
embind为许多标准C++类型提供类型转换
C++类型 | JavaScript类型 |
---|---|
void | undefined |
bool | true or false |
char | number |
signed char | number |
unsigned char | number |
short | number |
ungigned short | number |
int | number |
unsigned int | number |
log | number |
unsigned long | number |
float | number |
double | number |
std::string | ArrayBuffer, Uint8Array, Uint8ClampedArray, Int8Array, or String |
std::wstring | String (UTF-16 code units) |
emscripten::val | anything |
为了方便,embind还提供了工厂函数用来注册std::vector<T>(register_vector())和std::map<K, V>(register_map())类型。
EMSCRIPTEN_BINDINGS(stl_wrappers) {
register_vector<int>("VectorInt");
register_map<int,int>("MapIntInt");
}
性能
在撰写本文时,还没有对标准基准测试或相对于WebIDL Binder的全面的embind性能测试。
简单函数的调用开销在200ns左右。虽然还有进一步优化的空间,但到目前为止,它在实际应用程序中的性能已经被证明是完全可以接受的。
Emscripten代码移植系列文章
Emscripten代码移植主题系列文章是emscripten中文站点的一部分内容。
本文是第三个主题第二篇文章。
第一个主题介绍代码可移植性与限制
第二个主题介绍Emscripten的运行时环境
第三个主题第一篇文章介绍连接C++和JavaScript
第三个主题第二篇文章介绍embind
第四个主题介绍文件和文件系统
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。